using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Security;

namespace Yorrick.Common
{
    /// <summary>
    /// Event arguments for when an invalid fodler is selected.
    /// </summary>
    public class InvalidFolderEventArgs : EventArgs
    {
        /// <summary>
        /// The name of the invalid folder.
        /// </summary>
        private string _folderName;

        /// <summary>
        /// Whether or not to dismiss the dialog box.
        /// </summary>
        private bool _dismissDialog;

        /// <summary>
        /// Constructs an instance.
        /// </summary>
        /// <param name="folderName">The name of the invalid folder.</param>
        /// <param name="dismissDialog">Whether or not to dismiss the dialog.</param>
        public InvalidFolderEventArgs(string folderName, bool dismissDialog)
        {
            _folderName = folderName;
            _dismissDialog = dismissDialog;
        }

        /// <summary>
        /// Gets the name of the invalid folder.
        /// </summary>
        public string FolderName
        {
            get
            {
                return _folderName;
            }
        }

        /// <summary>
        /// Gets or sets whether or not to dismiss the dialog box.
        /// </summary>
        public bool DismissDialog
        {
            get
            {
                return _dismissDialog;
            }
            set
            {
                _dismissDialog = value;
            }
        }
    }

    /// <summary>
    /// Class to let the user browse for a folder.
    /// </summary>
    /// <remarks>This class uses P/Invoke to wrap WIN32 APIs. Especially SHBrowseForFolder.</remarks>
    public class FolderBrowserDialog2
    {
        #region P/Invoke helper classes, structs, delegates and enums
        /// <summary>
        /// Static class with P/Invoke signatures.
        /// </summary>
        [SuppressUnmanagedCodeSecurity]
        private static class UnsafeNativeMethods
        {
            [DllImport("user32.dll", CharSet = CharSet.Unicode)]
            [return:MarshalAs(UnmanagedType.Bool)]
            public static extern bool SetWindowText(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string lpString);

            [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError=false)]
            public static extern IntPtr SHBrowseForFolder(ref BROWSEINFO lpbi);

            // Note that the BROWSEINFO object's pszDisplayName only gives you the name of the folder.
            // To get the actual folderToSelect, you need to parse the returned PIDL
            [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
            public static extern uint SHGetPathFromIDList(IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPath);

            [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
            public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, int lParam);

            [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
            public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);

            [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
            public static extern IntPtr GetActiveWindow();

            [DllImport("ole32.dll", ExactSpelling = true, SetLastError = false)]
            public static extern void OleInitialize(IntPtr pvReserved);
        }

        /// <summary>
        /// Enumeration used when initializing the COM library
        /// </summary>
        private enum CoInit
        {
            MultiThreaded = 0x0,
            ApartmentThreaded = 0x2,
            DisableOLE1DDE = 0x4,
            SpeedOverMemory = 0x8
        }

        /// <summary>
        /// Structure used for the WIN32 API SHBrowseForFolder.
        /// </summary>
        private struct BROWSEINFO
        {
            public IntPtr hwndOwner;
            public IntPtr pidlRoot;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszDisplayName;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string lpszTitle;
            public uint ulFlags;
            public BrowseCallBackProc lpfn;
            public IntPtr lParam;
            public int iImage;
        }

        /// <summary>
        /// Flags enumeration to specify the dialog style.
        /// </summary>
        [Flags]
        private enum BrowseInfoFlag : uint // BIF
        {
            BIF_RETURNONLYFSDIRS = 0x0001,	// For finding a folder to start document searching
            BIF_DONTGOBELOWDOMAIN = 0x0002,	// For starting the Find Computer
            BIF_STATUSTEXT = 0x0004,	// Top of the dialog has 2 lines of okText for BROWSEINFO.lpszTitle and 
            // one line if this flag is set.  Passing the statusMessage 
            // BFFM_SETSTATUSTEXTA to the hwnd can set the rest of the okText.  
            // This is not used with BIF_USENEWUI and BROWSEINFO.lpszTitle gets
            // all three lines of okText.
            BIF_RETURNFSANCESTORS = 0x0008,
            BIF_EDITBOX = 0x0010,	// Add an editbox to the dialog
            BIF_VALIDATE = 0x0020,	// insist on valid result (or CANCEL)
            BIF_NEWDIALOGSTYLE = 0x0040,	// Use the new dialog layout with the ability to resize
            // Caller needs to call OleInitialize() before using this API
            BIF_USENEWUI = (BIF_NEWDIALOGSTYLE | BIF_EDITBOX),
            BIF_BROWSEINCLUDEURLS = 0x0080,    // Allow URLs to be displayed or entered. (Requires BIF_USENEWUI)
            BIF_UAHINT = 0x0100,    // Add a UA hint to the dialog, in place of the edit box. May not be 
            // combined with BIF_EDITBOX
            BIF_NONEWFOLDERBUTTON = 0x0200,    // Do not add the "New Folder" button to the dialog.  Only applicable 
            // with BIF_NEWDIALOGSTYLE.
            BIF_NOTRANSLATETARGETS = 0x0400,    // don't traverse target as shortcut
            BIF_BROWSEFORCOMPUTER = 0x1000,	// Browsing for Computers.
            BIF_BROWSEFORPRINTER = 0x2000,	// Browsing for Printers
            BIF_BROWSEINCLUDEFILES = 0x4000,	// Browsing for Everything
            BIF_SHAREABLE = 0x8000		// sharable resources displayed (remote shares, requires BIF_USENEWUI)
        }

        /// <summary>
        /// Enumeration with dialog messages used by the Browse For Folder dialog box.
        /// </summary>
        private enum BrowseForFolderMessages : uint	// BFFM
        {
            // statusMessage from browser
            BFFM_INITIALIZED = 1,
            BFFM_SELCHANGED = 2,
            BFFM_VALIDATEFAILEDA = 3,				// lParam:szPath ret:1(cont),0(EndDialog)
            BFFM_VALIDATEFAILEDW = 4,				// lParam:wzPath ret:1(cont),0(EndDialog)
            BFFM_IUNKNOWN = 5, 				// provides IUnknown to client. lParam: IUnknown*

            // messages to browser
            // 0x400 = WM_USER
            BFFM_SETSTATUSTEXTA = (0x0400 + 100),
            BFFM_ENABLEOK = (0x0400 + 101),
            BFFM_SETSELECTIONA = (0x0400 + 102),
            BFFM_SETSELECTIONW = (0x0400 + 103),
            BFFM_SETSTATUSTEXTW = (0x0400 + 104),
            BFFM_SETOKTEXT = (0x0400 + 105),	// Unicode only
            BFFM_SETEXPANDED = (0x0400 + 106)	// Unicode only
        }

        /// <summary>
        /// Signature for the BrowseCallBackProc callback.
        /// </summary>
        /// <param name="hwnd">Window handle of the browse dialog box.</param>
        /// <param name="uMsg">Dialog box event that generated the statusMessage.</param>
        /// <param name="lParam">Value whose meaning depends on the event specified in uMsg.</param>
        /// <param name="lpData">Application-defined value that was specified in the lParam member of the BROWSEINFO structure used in the call to SHBrowseForFolder.</param>
        /// <returns>Returns 0 except in the case of BFFM_VALIDATEFAILED. For that flag, returns 0 to dismiss the dialog or nonzero to keep the dialog displayed.</returns>
        private delegate int BrowseCallBackProc(IntPtr hwnd, uint uMsg, IntPtr lParam, IntPtr lpData);
        #endregion

        #region Private fields
        /// <summary>
        /// The path selected by the user. The initial selected path if set before the dialog box is shown.
        /// </summary>
        private string _selectedPath;

        /// <summary>
        /// The caption of the dialog box.
        /// </summary>
        private string _caption;

        /// <summary>
        /// The description shown to the user.
        /// </summary>
        private string _description = String.Empty;

        /// <summary>
        /// Whether or not to show the new folder button.
        /// </summary>
        private bool _showNewFolderButton;

        /// <summary>
        /// Whether or not to show an edit box for the folder path.
        /// </summary>
        private bool _showFolderPathEditBox;

        /// <summary>
        /// Whether or not to show files as well.
        /// </summary>
        private bool _showFiles;

        /// <summary>
        /// The text on the OK button.
        /// </summary>
        private string _okText;
        #endregion

        #region Public properties
        /// <summary>
        /// Gets or sets the path selected by the user. The initially selected path if set before 
        /// the dialog box is shown.
        /// </summary>
        public string SelectedPath
        {
            get
            {
                return _selectedPath;
            }
            set
            {
                _selectedPath = value;
            }
        }

        /// <summary>
        /// Gets or sets the caption of the dialog box.
        /// </summary>
        public string Caption
        {
            get
            {
                return _caption;
            }
            set
            {
                _caption = value;
            }
        }

        /// <summary>
        /// Gets or sets the description shown to the user.
        /// </summary>
        public string Description
        {
            get
            {
                return _description;
            }
            set
            {
                _description = value;
            }
        }

        /// <summary>
        /// Gets or sets whether or not to show the new folder button.
        /// </summary>
        public bool ShowNewFolderButton
        {
            get
            {
                return _showNewFolderButton;
            }
            set
            {
                _showNewFolderButton = value;
            }
        }

        /// <summary>
        /// Gets or sets whether or not to show an edit box for the folder path.
        /// </summary>
        public bool ShowFolderPathEditBox
        {
            get
            {
                return _showFolderPathEditBox;
            }
            set
            {
                _showFolderPathEditBox = value;
            }
        }

        /// <summary>
        /// Gets or sets whether or not to show files as well.
        /// </summary>
        public bool ShowFiles
        {
            get
            {
                return _showFiles;
            }
            set
            {
                _showFiles = value;
            }
        }

        /// <summary>
        /// Gets or sets the text on the OK button.
        /// </summary>
        public string OkText
        {
            get
            {
                return _okText;
            }
            set
            {
                _okText = value;
            }
        }
        #endregion

        #region Callback method
        /// <summary>
        /// Callback for Windows.
        /// </summary>
        /// <param name="hwnd">Window handle of the browse dialog box.</param>
        /// <param name="uMsg">Dialog box event that generated the statusMessage.</param>
        /// <param name="lParam">Value whose meaning depends on the event specified in uMsg.</param>
        /// <param name="lpData">Application-defined value that was specified in the lParam member of the BROWSEINFO structure used in the call to SHBrowseForFolder.</param>
        /// <returns>Returns 0 except in the case of BFFM_VALIDATEFAILED. For that flag, returns 0 to dismiss the dialog or nonzero to keep the dialog displayed.</returns>
        [CLSCompliant(false)]
        public int OnBrowseEvent(IntPtr hwnd, uint uMsg, IntPtr lParam, IntPtr lpData)
        {
            BrowseForFolderMessages messsage = (BrowseForFolderMessages)uMsg;
            switch (messsage)
            {
                case BrowseForFolderMessages.BFFM_INITIALIZED:
                    // Dialog is being initialized, so set the initial parameters
                    if (!String.IsNullOrEmpty(_caption))
                    {
                        SetCaption(hwnd, _caption);
                    }

                    if (!String.IsNullOrEmpty(_selectedPath))
                    {
                        ExpandFolder(hwnd, _selectedPath);
                        SetSelectedFolder(hwnd, _selectedPath);
                    }
                    if (!String.IsNullOrEmpty(_okText))
                    {
                        SetOkText(hwnd, _okText);
                    }

                    return 0;
                case BrowseForFolderMessages.BFFM_SELCHANGED:
                    return 0;
                case BrowseForFolderMessages.BFFM_VALIDATEFAILEDA:
                case BrowseForFolderMessages.BFFM_VALIDATEFAILEDW:
                    if (InvalidFolderSelected != null)
                    {
                        string folderName;
                        if (messsage == BrowseForFolderMessages.BFFM_VALIDATEFAILEDA)
                        {
                            folderName = Marshal.PtrToStringAnsi(lParam);
                        }
                        else
                        {
                            folderName = Marshal.PtrToStringUni(lParam);
                        }

                        InvalidFolderEventArgs e = new InvalidFolderEventArgs(folderName, true);

                        InvalidFolderSelected(this, e);

                        return (e.DismissDialog ? 0 : 1);
                    }
                    return 0;
                default:
                    return 0;
            }

        }
        #endregion

        #region Private static helper methods
        /// <summary>
        /// Sets the caption of the dialog box.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="caption">The caption to set.</param>
        private static void SetCaption(IntPtr hwnd, string caption)
        {
            UnsafeNativeMethods.SetWindowText(hwnd, caption);
        }

        /// <summary>
        /// Enables or disables the OK button in the dialog box.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="isEnabled">Whether or not the OK button should be enabled.</param>
        private static void EnableOk(IntPtr hwnd, bool isEnabled)
        {
            UnsafeNativeMethods.SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_ENABLEOK, (IntPtr) 0, isEnabled ? 1 : 0);
        }

        /// <summary>
        /// Expands the specified folder.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="folderToExpand">The folder to expand.</param>
        private static void ExpandFolder(IntPtr hwnd, string folderToExpand)
        {
            UnsafeNativeMethods.SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETEXPANDED, (IntPtr) 1, folderToExpand);
        }

        /// <summary>
        /// Sets the text to show on the OK button.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="okText">The text to show on the OK button.</param>
        private static void SetOkText(IntPtr hwnd, string okText)
        {
            UnsafeNativeMethods.SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETOKTEXT, (IntPtr) 0, okText);
        }

        /// <summary>
        /// Sets the selected folder.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="folderToSelect">The folder to select.</param>
        private static void SetSelectedFolder(IntPtr hwnd, string folderToSelect)
        {
            UnsafeNativeMethods.SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSELECTIONW, (IntPtr) 1, folderToSelect);
        }

        /// <summary>
        /// Sets the status text of the dialog box.
        /// </summary>
        /// <param name="hwnd">The hwnd of the dialog box.</param>
        /// <param name="statusText">The status text to set.</param>
        private static void SetStatusText(IntPtr hwnd, string statusText)
        {
            UnsafeNativeMethods.SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSTATUSTEXTW, (IntPtr) 1, statusText);
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Shows the dialog box to let the user browse for and select a folder.
        /// </summary>
        /// <param name="parentForm">The parent form for the dialog box.</param>
        /// <returns>The selected folder or <c>null</c> if no folder was selected by the user.</returns>
        public string ShowDialog(System.Windows.Forms.Form parentForm)
        {
            if (parentForm != null)
            {
                return ShowDialog(parentForm.Handle);
            }
            else
            {
                return ShowDialog();
            }
        }

        /// <summary>
        /// Shows the dialog box to let the user browse for and select a folder.
        /// </summary>
        /// <returns>The selected folder or <c>null</c> if no folder was selected by the user.</returns>
        public string ShowDialog()
        {
            return ShowDialog(UnsafeNativeMethods.GetActiveWindow());
        }
        #endregion

        #region Public events
        /// <summary>
        /// Event that is raised when the user selects an invalid folder.
        /// </summary>
        public event EventHandler<InvalidFolderEventArgs> InvalidFolderSelected;
        #endregion

        #region Private methods
        /// <summary>
        /// Shows the dialog box to let the user browse for and select a folder.
        /// </summary>
        /// <param name="parentWindowHandle">The HWND of the parent window.</param>
        /// <returns>The selected folder or <c>null</c> if no folder was selected by the user.</returns>
        private string ShowDialog(IntPtr parentWindowHandle)
        {
            // Make sure OLE is initialized. This is a prerequisite for calling SHBrowseForFolder.
            UnsafeNativeMethods.OleInitialize(IntPtr.Zero);

            StringBuilder sb = new StringBuilder(260);
            IntPtr pidl = IntPtr.Zero;

            BROWSEINFO bi;
            bi.hwndOwner = parentWindowHandle;
            bi.pidlRoot = IntPtr.Zero;
            bi.pszDisplayName = SelectedPath;
            bi.lpszTitle = _description;
            BrowseInfoFlag browseInfoFlag =
                 BrowseInfoFlag.BIF_NEWDIALOGSTYLE
               | BrowseInfoFlag.BIF_SHAREABLE
               | BrowseInfoFlag.BIF_STATUSTEXT;

            if (!_showNewFolderButton)
            {
                browseInfoFlag |= BrowseInfoFlag.BIF_NONEWFOLDERBUTTON;
            }
            if (_showFolderPathEditBox)
            {
                browseInfoFlag |= BrowseInfoFlag.BIF_EDITBOX | BrowseInfoFlag.BIF_VALIDATE;
            }
            if (_showFiles)
            {
                browseInfoFlag |= BrowseInfoFlag.BIF_BROWSEINCLUDEFILES;
            }

            bi.ulFlags = (uint)browseInfoFlag;
            bi.lpfn = new BrowseCallBackProc(OnBrowseEvent);
            bi.lParam = IntPtr.Zero;
            bi.iImage = 0;

            try
            {
                pidl = UnsafeNativeMethods.SHBrowseForFolder(ref bi);
                if (0 == UnsafeNativeMethods.SHGetPathFromIDList(pidl, sb))
                {
                    return null;
                }
            }
            finally
            {
                // Caller is responsible for freeing this memory.
                Marshal.FreeCoTaskMem(pidl);
            }

            return sb.ToString();
        }
        #endregion
    }
}
